Skip to content

Migrate template widget configuration to Compose and Material3 - #7288

Open
su7ri wants to merge 14 commits into
home-assistant:mainfrom
su7ri:compose-template-widget-6304
Open

Migrate template widget configuration to Compose and Material3#7288
su7ri wants to merge 14 commits into
home-assistant:mainfrom
su7ri:compose-template-widget-6304

Conversation

@su7ri

@su7ri su7ri commented Aug 1, 2026

Copy link
Copy Markdown

Summary

Migrates TemplateWidgetConfigureActivity from a legacy XML/View Binding
layout to Jetpack Compose with Material 3, following the same pattern
established for the entity widget in #7007 (referenced from #6307 as the
example to follow for this series of widget migrations).

TemplateWidgetConfigureActivity.kt is now a thin wrapper (~60 lines,
down from ~250) around:

  • TemplateWidgetConfigureState: immutable UI state, including a
    TemplatePreview sealed type (Empty/Rendered/Error) replacing the old
    single TextView that mixed rendered output, error messages, and the
    "blank" placeholder.
  • TemplateWidgetConfigureViewModel: the restore/save/render logic,
    moved out of the Activity into a Hilt ViewModel with StateFlow.
  • TemplateWidgetConfigureScreen: the Compose UI, reusing the shared
    WidgetBackgroundTypeDropdown / WidgetTextColorDropdown composables.

The live template preview still renders basic HTML from the server
response; the HTML→AnnotatedString conversion follows the same approach
already used on the Wear OS template tile screen
(SettingsWearTemplateTile.kt), duplicated here rather than extracted
to keep this PR focused on the widget migration. Happy to extract it to
a shared util in a follow-up if maintainers prefer.

Closes #6304

Checklist

  • New or updated tests have been added to cover the changes following the testing guidelines.
  • The code follows the project's code style and best_practices.
  • The changes have been thoroughly tested, and edge cases have been considered.
  • Changes are backward compatible whenever feasible. Any breaking changes are documented in the changelog for users and/or in the code for developers depending on the relevance.
  • I have read the Open Home Foundation AI Policy.

Select exactly one option that describes AI usage in this contribution:

  • I have not used AI for this contribution.
  • AI assistance was used for this contribution.
  • AI fully generated the code for this contribution, but I've reviewed and understood it before submitting and will respond without AI during review.

Screenshots

Screenshot 2026-08-01 at 10 28 48

Any other notes

Part of the ongoing effort to migrate widget configuration screens to
Compose/Material 3 (see #6303, #6305, #6306, #6308, and others).

Copilot AI review requested due to automatic review settings August 1, 2026 09:30

@home-assistant home-assistant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @su7ri

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

@home-assistant

home-assistant Bot commented Aug 1, 2026

Copy link
Copy Markdown

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@home-assistant
home-assistant Bot marked this pull request as draft August 1, 2026 09:30
@su7ri
su7ri marked this pull request as ready for review August 1, 2026 09:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

The ViewModel can leave a stale “Rendered” preview while a new render is in-flight, allowing users to save an unvalidated template/server selection before rendering completes.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

Migrates the Template widget configuration flow from a legacy XML/ViewBinding Activity to a Compose + Material3 screen driven by an assisted Hilt ViewModel, aligning the widget configuration architecture with the recent entity widget migration pattern.

Changes:

  • Replaced the legacy TemplateWidgetConfigureActivity UI with a Compose screen (TemplateWidgetConfigureScreen) under HATheme.
  • Introduced TemplateWidgetConfigureViewModel + immutable TemplateWidgetConfigureState (including a TemplatePreview sealed type) to own restore/render/save logic via StateFlow.
  • Added focused unit tests for restore/render/save behavior; removed the obsolete XML layout.
File summaries
File Description
app/src/main/kotlin/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureActivity.kt Converts the Activity into a thin Compose wrapper that delegates logic to the ViewModel.
app/src/main/kotlin/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureScreen.kt New Compose UI for configuring the widget, including HTML preview rendering.
app/src/main/kotlin/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureState.kt Adds immutable UI state and a sealed preview model for rendered/empty/error states.
app/src/main/kotlin/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureViewModel.kt New assisted Hilt ViewModel handling restore/render/save and widget pin/update flows.
app/src/test/kotlin/io/homeassistant/companion/android/widgets/template/TemplateWidgetConfigureViewModelTest.kt Adds unit tests for state restoration, rendering, and persistence behavior.
app/src/main/res/layout/widget_template_configure.xml Removes the legacy XML configuration layout.
Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@jpelgrom jpelgrom left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for picking this up!

Could you add a screenshot test as well? You already have a Compose preview so it shouldn't be too difficult :)

The live template preview still renders basic HTML from the server response; the HTML→AnnotatedString conversion follows the same approach already used on the Wear OS template tile screen (SettingsWearTemplateTile.kt), duplicated here rather than extracted to keep this PR focused on the widget migration. Happy to extract it to a shared util in a follow-up if maintainers prefer.

As it is only ~30 lines you can extract it into a util file in this PR instead of duplicating code.

Fixes an Android Lint (Slack compose-lints) finding: composables should
only emit content from one source at their top level.

@jpelgrom jpelgrom left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost there!

Compared to the old activity text alignment via HTML no longer shows in the preview but as it's quite an advanced feature and still working in the widget it's fine as it is right now so we can avoid complicating it even more. If more users show up with complicated HTML examples we can always fallback to a classic view which takes the HTML.

Before After
Image Image(theptag is adding padding at the bottom)

Comment on lines +215 to +218
modifier = Modifier.size(HAPreviewIconSize),
)
Text(
text = stringResource(commonR.string.template_preview_label),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would look slightly better if it's the size of a title in a dropdown, what do you think?

Image
Suggested change
modifier = Modifier.size(HAPreviewIconSize),
)
Text(
text = stringResource(commonR.string.template_preview_label),
modifier = Modifier.size(HASize.M),
)
Text(
text = stringResource(commonR.string.template_preview_label),
style = HATextStyle.BodyMedium,

Don't forget to update screenshot tests if you accept.

If you don't agree update the size to use one of our shared constants:

Suggested change
modifier = Modifier.size(HAPreviewIconSize),
)
Text(
text = stringResource(commonR.string.template_preview_label),
modifier = Modifier.size(HASize.L),
)
Text(
text = stringResource(commonR.string.template_preview_label),

import io.homeassistant.companion.android.common.compose.composable.HATextField
import io.homeassistant.companion.android.common.compose.composable.HATopBar
import io.homeassistant.companion.android.common.compose.theme.HADimens
import io.homeassistant.companion.android.common.compose.theme.HAThemeForPreview

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you agree with https://github.com/home-assistant/android/pull/7288/changes#r3714512883

Suggested change
import io.homeassistant.companion.android.common.compose.theme.HAThemeForPreview
import io.homeassistant.companion.android.common.compose.theme.HATextStyle
import io.homeassistant.companion.android.common.compose.theme.HAThemeForPreview

@TimoPtr

TimoPtr commented Aug 4, 2026

Copy link
Copy Markdown
Member

I forgot we should have compose tests that verify that the callbacks are properly triggered and also the content is adjusting properly based on the UI State like showing the server selector, the text size error, the action button has the right text and it is disabled/enabled on the right conditions.

Addresses TimoPtr's review comment requesting coverage for the shared
HTML-to-AnnotatedString conversion used by rendered templates.
claude added 6 commits August 4, 2026 22:26
- Drop the enabled=items.isNotEmpty() guard on the server selector,
  matching jpelgrom's resolution that show/enable should share the
  view state's own showServerSelector condition.
- Turn TemplateSection/AppearanceSection into ColumnScope receivers
  instead of wrapping their content in an extra Column.
- Move the text size field into AppearanceSection.
- Use HASize.M for the preview icon and HATextStyle.BodyMedium for
  the preview label/text.
- Clarify HATextField's singleLine/maxLines doc to mention the
  default relationship between the two.
- Add ViewModel test coverage verifying renderTemplate is/isn't
  invoked on blank vs. non-blank templates, and that a superseded
  render isn't left running; move BLACK_HEX out of a companion object.
- Add a Compose UI test for TemplateWidgetConfigureContent verifying
  callbacks fire and content adjusts to state (navigation, server
  selection, template/text-size input, background/text-color pickers,
  preview states, action enablement).

Reference screenshots under screenshotTestFullDebug still reflect the
pre-refactor layout/styling and need regenerating in an environment
with access to the Google Maven repository, which this sandbox lacks.
Addresses jpelgrom's comment on TEMPLATE_FIELD_MIN_LINES and
TimoPtr's comment on BLACK_HEX: both belong before the first
function/class rather than at the bottom of the file.
… size

- Memoize the HTML parse in TemplatePreviewCard on the rendered text,
  since it doesn't need to re-run on every recomposition. Only the
  parsing itself is remembered, not the whole toAnnotatedString() call,
  since remember's calculation lambda disallows the composable
  stringResource() calls used by the other TemplatePreview branches.
- isActionEnabled now also requires a valid text size, so an emptied
  size field can't silently fall back to the default on save.
- DEFAULT_TEXT_SIZE is only used within its own file, so narrow it
  from internal to private.
- Move restoreConfiguration() and renderTemplate() to the bottom of
  TemplateWidgetConfigureViewModel, after the public/internal API,
  per TimoPtr's comment (private functions go last even when used
  from the constructor/init block).
- getPendingDaoEntity() is only used within the ViewModel, so narrow
  it to private and move it next to the other private members at the
  bottom of the file.
- Match HATextField's minLines doc wording to maxLines'.
- Add coVerify assertions that clearing/blanking the template doesn't
  trigger an extra render call.
- Add a single-server screenshot test demonstrating the server
  selector stays hidden, and use a more representative rendered
  template (color span, bold, line break) for the existing screenshot
  tests instead of a bare number.
@TimoPtr

TimoPtr commented Aug 5, 2026

Copy link
Copy Markdown
Member

Thanks for taking the time to contribute.

This contribution looks like it doesn't follow our AI policy, so I'm flagging it here. If I've got this wrong, please let me know.

We won't close it this time because @jpelgrom and myself already reviewed it, but please keep this in consideration for future contributions to this PR or future PRs.

}
}

private companion object {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need to be in a companion object

@su7ri

su7ri commented Aug 5, 2026

Copy link
Copy Markdown
Author

Thanks for taking the time to contribute.

This contribution looks like it doesn't follow our AI policy, so I'm flagging it here. If I've got this wrong, please let me know.

We won't close it this time because @jpelgrom and myself already reviewed it, but please keep this in consideration for future contributions to this PR or future PRs.

Thanks for flagging this. I'll keep using AI as a tool since I'm still learning this codebase, but I'll make sure to actually understand and review each change before it goes up, and commit things myself instead of letting the tool push directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rework UI of TemplateWidgetConfigureActivity to compose and Material3

6 participants